--[[
ClosetGnome, a World of Warcraft addon to manage item sets.
Copyright (C) 2007 Rabbit.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
]]

-- AceConfigRegistry doesn't allow spaces in option keys, so here we just
-- nil out their whole validate method until that's fixed and the spec is
-- changed to allow spaces.
do
	local acr, version = LibStub("AceConfigRegistry-3.0")
	if version < 7 then
		local orig = acr.ValidateOptionsTable
		acr.ValidateOptionsTable = function() return true end
	end
end

-------------------------------------------------------------------------------
-- Locals                                                                    --
-------------------------------------------------------------------------------

local L = LibStub("AceLocale-3.0"):GetLocale("ClosetGnome")

-- GRRRR! We shouldn't need this variable, but BLIZZARD IS ON CRACK.
local createOrUpdateSetName = nil

local deequipQueue = nil
local queuedSet = {}
local slotLocks = nil
local queuedForDelete = nil
local activeSlots = nil
local missingItems = nil
local equipQueue = nil

local db = nil

local keybindingFrame = "ClosetGnomeKeybindingFrame"

local _G = getfenv(0)

local slots = {
	[1] = {name = "Head"},
	[3] = {name = "Shoulder"},
	[5] = {name = "Chest"},
	[6] = {name = "Waist"},
	[7] = {name = "Legs"},
	[8] = {name = "Feet"},
	[9] = {name = "Wrist"},
	[10] = {name = "Hands"},
	[17] = {name = "SecondaryHand"},
	[16] = {name = "MainHand"},
	[18] = {name = "Ranged"},
	[0] = {name = "Ammo"},
	[2] = {name = "Neck"},
	[15] = {name = "Back"},
	[11] = {name = "Finger0"},
	[12] = {name = "Finger1"},
	[13] = {name = "Trinket0"},
	[14] = {name = "Trinket1"},
	[19] = {name = "Tabard", inactive = true},
	[4] = {name = "Shirt", inactive = true},
}

local NUM_QUIPS = L["num_quips"]

local PickupContainerItem = _G.PickupContainerItem
local AutoEquipCursorItem = _G.AutoEquipCursorItem
local EquipCursorItem = _G.EquipCursorItem
local PickupInventoryItem = _G.PickupInventoryItem

local function getInvItem(s)
	return GetInventoryItemLink("player", s)
end

local function reclaim(t)
	if type(t) == "table" then wipe(t) end
	t=nil;return t
end

-------------------------------------------------------------------------------
-- Addon declaration                                                         --
-------------------------------------------------------------------------------

ClosetGnome = LibStub("AceAddon-3.0"):NewAddon("ClosetGnome", "AceEvent-3.0", "AceHook-3.0", "AceTimer-3.0")
local ClosetGnome = ClosetGnome
local callbacks = LibStub("CallbackHandler-1.0"):New(ClosetGnome)
local icon = LibStub("LibDBIcon-1.0", true)

ClosetGnome.revision = tonumber(string.sub("$Revision: 587 $", 12, -3))

local compareItems -- forward declaration of the function

local options = {
	type = "group",
	args = {
		description = {
			type = "description",
			name = L["closetgnome_description"],
			order = 1,
		},
		alwaysWeapons = {
			type = "toggle",
			name = L["Always equip weapons"],
			desc = L["Equip weapons in sets even if you are in combat."],
			get = function() return db.alwaysWeapons end,
			set = function(info, v) db.alwaysWeapons = v end,
			order = 10,
			width = "full",
		},
		quips = {
			type = "toggle",
			name = L["Quips"],
			desc = L["Toggle outputting random quips when equipping sets."],
			get = function() return ClosetGnome.db.profile.quips end,
			set = function(info, v) ClosetGnome.db.profile.quips = v end,
			order = 20,
			width = "full",
		},
		minimap = {
			type = "toggle",
			name = L["Show minimap icon"],
			desc = L["Toggle showing or hiding the minimap icon."],
			get = function() return not ClosetGnome.db.profile.minimap.hide end,
			set = function(info, v)
				local hide = not v
				ClosetGnome.db.profile.minimap.hide = hide
				if hide then
					icon:Hide("ClosetGnome")
				else
					icon:Show("ClosetGnome")
				end
			end,
			order = 30,
			width = "full",
			hidden = function() return not icon or not CGLDB or not icon:IsRegistered("ClosetGnome") end,
		},
	},
}
local keybindingOptions = {
	type = "group",
	args = {
		kbDesc = { -- Means you can't have a set called kbDesc :p
			type = "description",
			name = L["keybinding_description"],
			order = 101,
		},
	},
}

-------------------------------------------------------------------------------
-- Triggers
-------------------------------------------------------------------------------

local function triggerEvent(...)
	callbacks:Fire(...)
end

-------------------------------------------------------------------------------
-- Keybindings                                                               --
-------------------------------------------------------------------------------

local function KeybindingClicked()
	if type(this.set) ~= "string" then return end
	ClosetGnome:WearSet(this.set)
end

local function unsetBinding(set)
	if not set or not db.keybindings[set] then return end
	local combo = db.keybindings[set]

	local frame = _G[keybindingFrame..combo]
	if frame then
		frame:SetScript("OnMouseUp", nil)
		frame = nil
	end

	SetBinding(combo) -- Unset the binding.

	triggerEvent("KeybindingsUpdated")
end

local function registerBinding(combo, set)
	if not combo or not set then return end

	local shouldPrint = true
	for k, v in pairs(db.keybindings) do
		if v == combo then
			shouldPrint = nil
		elseif k == set then
			unsetBinding(set)
		end
	end

	if shouldPrint then
		print(L["Registering keybinding %s to set %s."]:format("|cffd9d919"..combo.."|r", "|cffd9d919"..set.."|r"))
	end

	local frameName = keybindingFrame..combo
	local frame = _G[frameName] or CreateFrame("Button", frameName, _G["UIParent"])
	frame.combo = combo
	frame.set = set
	frame:SetScript("OnMouseUp", KeybindingClicked)

	db.keybindings[set] = combo
	SetBindingClick(combo, frameName)

	triggerEvent("KeybindingsUpdated")
end

local function clearBinding(set)
	if not set or not db.keybindings[set] then return end

	unsetBinding(set)

	print(L["Removing keybinding %s from set %s."]:format("|cffd9d919"..db.keybindings[set].."|r", "|cffd9d919"..set.."|r"))
	db.keybindings[set] = nil

	triggerEvent("KeybindingsUpdated")
end

-------------------------------------------------------------------------------
-- Menu                                                                      --
-------------------------------------------------------------------------------

local function updateSetOptions()
	local counter = 150
	for k, v in pairs(keybindingOptions.args) do
		if k ~= "kbDesc" then
			v.hidden = true
		end
	end
	for k, v in pairs(db.set) do
		
		if keybindingOptions.args[k] then
			keybindingOptions.args[k].hidden = false
			keybindingOptions.args[k .. "_label"].hidden = false
			keybindingOptions.args[k .. "_spacer"].hidden = false
		else
			keybindingOptions.args[k .. "_label"] = {
				type = "description",
				name = "",
				width = "half",
				-- image = .... set below 
			}
			keybindingOptions.args[k] = {
				type = "keybinding",
				name = k,
				desc = L["Assign a keybinding to %s."]:format(k),
				get = function() return db.keybindings[k] end,
				set = function(info, value)
					if not value or value:len() < 1 then
						clearBinding(k)
					else
						registerBinding(value, k)
					end
				end,
			}
			keybindingOptions.args[k .. "_spacer"] = {
				type = "description",
				name = "",
			}
		end
		
		keybindingOptions.args[k .. "_label"].order = counter
		keybindingOptions.args[k .. "_label"].image = db.icons[k] or "Interface\\Icons\\INV_Chest_Cloth_17"
		keybindingOptions.args[k].order = counter + 1
		keybindingOptions.args[k .. "_spacer"].order = counter + 2
		
		counter = counter + 10
	end
end

-------------------------------------------------------------------------------
-- Initialization                                                            --
-------------------------------------------------------------------------------

local defaults = {
	profile = {
		quips = true,
		minimap = {
			hide = true,
		},
	},
	char = {
		set = {},
		icons = {},
		keybindings = {},
		alwaysWeapons = true,
	}
}

function ClosetGnome:OnInitialize()
	self.version = (self.version or "1").."."..tostring(self.revision or "1")

	_G.CLOSETGNOME_VERSION = self.version
	_G.CLOSETGNOME_REVISION = self.revision

	self.db = LibStub("AceDB-3.0"):New("ClosetGnomeDB", defaults)

	db = self.db.char

	LibStub("AceConfig-3.0"):RegisterOptionsTable("ClosetGnome", options)
	LibStub("AceConfig-3.0"):RegisterOptionsTable("ClosetGnome-Keybindings", keybindingOptions)
	LibStub("AceConfigDialog-3.0"):AddToBlizOptions("ClosetGnome", "ClosetGnome")
	LibStub("AceConfigDialog-3.0"):AddToBlizOptions("ClosetGnome-Keybindings", L["Keybindings"], "ClosetGnome")
end

function ClosetGnome:OnEnable()
	if type(_G.StaticPopupDialogs) ~= "table" then
		_G.StaticPopupDialogs = {}
	end
	if not StaticPopupDialogs["ClosetGnomeAdd"] then
		StaticPopupDialogs["ClosetGnomeAdd"] = {
			text = L["|cffeda55fLeft-Click|r a slot to toggle it for this set. Green slots are |cff00ff00enabled|r, red are |cffff0000disabled|r, yellow are |cffffff00missing items|r.\n\nIf you want to assign an |cff0000fficon|r to this set, you can |cffeda55fCtrl-Click|r a slot to use that slots item as the icon.\n\nType the desired set name below and click Add when you are done."],
			button1 = L["Add"],
			button2 = L["Cancel"],
			OnCancel = function() self:CancelCreateSet() end,
			sound = "levelup2",
			whileDead = 1,
			hideOnEscape = 1,
			timeout = 0,
			OnShow = function()
				-- We have to do this onshow to reset the previous text
				_G[this:GetName().."EditBox"]:SetText(createOrUpdateSetName or "")
			end,
			OnHide = function()
				_G[this:GetName().."EditBox"]:SetText("")
			end,
			EditBoxOnEnterPressed = function()
				local name = _G[this:GetParent():GetName().."EditBox"]:GetText()
				self:AddSetFromDoll(name)
				createOrUpdateSetName = nil
				this:GetParent():Hide()
			end,
			EditBoxOnEscapePressed = function()
				self:CancelCreateSet()
				this:GetParent():Hide()
			end,
			OnAccept = function()
				local name = _G[this:GetParent():GetName().."EditBox"]:GetText()
				self:AddSetFromDoll(name)
				createOrUpdateSetName = nil
			end,
			hasEditBox = 1,
		}
	end
	if not StaticPopupDialogs["ClosetGnomeDelete"] then
		StaticPopupDialogs["ClosetGnomeDelete"] = {
			text = L["Are you sure you want to delete the set %s?"],
			button1 = L["Delete"],
			button2 = L["Cancel"],
			showAlert = 1,
			timeout = 0,
			OnAccept = function() self:DeleteSet(queuedForDelete, true) end,
			OnCancel = function() queuedForDelete = nil end,
		}
	end

	-- Create the button frames for keybindings
	for k, v in pairs(db.keybindings) do
		registerBinding(v, k)
	end

	updateSetOptions()

	triggerEvent("Initialized")
end

local function LockSlot(bag, slot)
	if not slotLocks then slotLocks = {} end
	if not slotLocks[bag] then slotLocks[bag] = {} end
	slotLocks[bag][slot] = true
end

local function IsSlotLocked(bag, slot)
	if slotLocks and slotLocks[bag] and slotLocks[bag][slot] then
		return true
	end
	return false
end

local function ClearSlotLocks()
	slotLocks = reclaim(slotLocks)
end

function ClosetGnome:OnDisable()
	activeSlots = reclaim(activeSlots)
	missingItems = reclaim(missingItems)
	ClearSlotLocks()

	for k, v in pairs(db.keybindings) do
		unsetBinding(k)
	end
end

-------------------------------------------------------------------------------
-- Addon methods                                                             --
-------------------------------------------------------------------------------

local currentlyMouseOveredSlot = nil
do
	local green = L["|cff00ff00Green slots|r are active, and any item currently in a green slot will be saved as part of the item set."]
	local red = L["|cffff0000Red slots|r are disabled, and will be ignored by the item set."]
	local yellow = L["A |cffffff00yellow slot|r means the item is missing from your inventory, and when you update the set, the item stored in the set will be used, and not the currently equipped one."]
	local blue = L["A |cff0000ffblue slot|r tells your ClosetGnome to use the item in that slot as the set icon. If a slot is toggled to an icon slot, it is also activated immediately."]
	function ClosetGnome:OnTooltipSetItem(tooltip)
		if not activeSlots then return end
		if activeSlots.icon and activeSlots.icon == currentlyMouseOveredSlot then
			tooltip:AddLine(blue, 0.35, 0.6, 0.95, true)
		elseif activeSlots[currentlyMouseOveredSlot] then
			tooltip:AddLine(green, 0.35, 0.6, 0.95, true)
		elseif missingItems and missingItems[currentlyMouseOveredSlot] then
			tooltip:AddLine(yellow, 0.35, 0.6, 0.95, true)
		elseif type(activeSlots[currentlyMouseOveredSlot]) == "boolean" and not activeSlots[currentlyMouseOveredSlot] then
			tooltip:AddLine(red, 0.35, 0.6, 0.95, true)
		end
	end
end

local function OnEnter(self)
	currentlyMouseOveredSlot = self.id
	if type(PaperDollItemSlotButton_OnEnter) == "function" then
		PaperDollItemSlotButton_OnEnter(self:GetParent())
	end
end

local function OnLeave(self)
	currentlyMouseOveredSlot = nil
	self:GetParent().updateTooltip = nil
	GameTooltip:Hide()
	ResetCursor()
end

local function OnClick(self, button)
	local id = self.id
	if IsShiftKeyDown() or (not IsModifierKeyDown() and CursorHasItem()) and type(PaperDollItemSlotButton_OnClick) == "function" then
		PaperDollItemSlotButton_OnClick(self:GetParent(), "LeftButton")
	elseif IsControlKeyDown() then
		-- Don't want an empty slot as icon.
		local currentItem = getInvItem(id)
		if currentItem == false then return end

		if activeSlots["icon"] then
			local oldSlot = activeSlots["icon"]
			activeSlots[oldSlot] = true
			_G["ClosetGnomeTexture"..oldSlot]:SetVertexColor(0, 255, 0)
		end
		activeSlots["icon"] = id
		activeSlots[id] = true
		self.texture:SetVertexColor(0, 0, 255)
	else
		activeSlots[id] = not activeSlots[id]
		if activeSlots[id] then
			if activeSlots["icon"] == id then
				self.texture:SetVertexColor(0, 0, 255)
			else
				missingItems[id] = nil
				self.texture:SetVertexColor(0, 255, 0)
			end
		else
			self.texture:SetVertexColor(255, 0, 0)
		end
	end
end

-- Opens the character frame and lets the user uncheck the items he doesn't
-- want.
function ClosetGnome:CreateOrUpdateSet(input)
	if UnitAffectingCombat("player") or InCombatLockdown() then return end

	activeSlots = {}
	missingItems = {}
	for id, slot in pairs(slots) do
		local gslot = _G["Character" .. slot.name .. "Slot"]
		if not slot.frame and gslot then
			-- We used to just :GetScript("OnClick") on the existing slot,
			-- and save that so that we could override it and put it back
			-- after we close the character frame, but this is not possible
			-- in TBC with the protected code stuff. WoW will complain that
			-- the script is tainted, and the normal slots won't work again
			-- after adding a set, so we create our own frame that we set on
			-- top of the existing one now.
			local button = CreateFrame("Button", "ClosetGnome"..id, gslot)
			button:SetAlpha(0)
			button:SetHeight(gslot:GetHeight())
			button:SetWidth(gslot:GetWidth())
			button:SetPoint("CENTER", gslot, "CENTER", 0, 1)
			button:SetScript("OnEnter", OnEnter)
			button:SetScript("OnLeave", OnLeave)
			button:SetScript("OnClick", OnClick)
			button.id = id
			button:Hide()

			local texture = gslot:CreateTexture("ClosetGnomeTexture"..id, "OVERLAY")
			texture:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
			texture:SetVertexColor(0, 255, 0)
			texture:SetBlendMode("ADD")
			texture:SetAlpha(0.75)
			texture:SetHeight(68)
			texture:SetWidth(68)
			texture:SetPoint("CENTER", gslot, "CENTER", 0, 1)
			texture:Hide()

			button.texture = texture

			slot.frame = button
			slot.texture = texture
		end
		if slot.frame and gslot then
			if slot.inactive then
				activeSlots[id] = false
				slot.texture:SetVertexColor(255, 0, 0)
			else
				activeSlots[id] = true
				slot.texture:SetVertexColor(0, 255, 0)
			end
			slot.frame:EnableMouse(true)
			slot.frame:Show()
			slot.texture:Show()
		end
	end

	-- We're updating a set!
	if type(input) == "string" and db.set[input] then
		self:WearSet(input)
		local set = db.set[input]
		for k in pairs(activeSlots) do
			if set[k] ~= nil then -- Can also be false, so check for nil.
				activeSlots[k] = true
				slots[k].texture:SetVertexColor(0, 255, 0)
			else
				activeSlots[k] = false
				slots[k].texture:SetVertexColor(255, 0, 0)
			end
		end
	end

	-- Hook Fizzle to prevent it from styling the slots while we add a set.
	if IsAddOnLoaded("Fizzle") and type(Fizzle) == "table" and type(Fizzle.UpdateItems) == "function" then
		if type(Fizzle.HideBorders) == "function" then
			Fizzle:HideBorders()
		end
		self:Hook(Fizzle, "UpdateItems", "Noop")
	end

	if IsAddOnLoaded("oGlow") and type(oGlow) == "table" and getmetatable(oGlow) then
		oGlow.preventCharacter = true
		if type(oGlow.updateCharacter) == "function" then
			oGlow.updateCharacter()
		end
	end

	PanelTemplates_SetTab(CharacterFrame, PaperDollFrame:GetID())
	if CharacterFrame:IsVisible() then
		if not PaperDollFrame:IsVisible() then
			CharacterFrame_ShowSubFrame("PaperDollFrame")
		end
	else
		ShowUIPanel(CharacterFrame)
		CharacterFrame_ShowSubFrame("PaperDollFrame")
	end

	OpenAllBags(1)

	if not self:IsHooked("CharacterFrame_OnHide") then
		self:Hook("CharacterFrame_OnHide", true)
	end
	if not self:IsHooked("CharacterFrameTab_OnClick") then
		self:Hook("CharacterFrameTab_OnClick", "Noop", true)
	end
	if not self:IsHooked("PaperDollItemSlotButton_OnModifiedClick") then
		self:Hook("PaperDollItemSlotButton_OnModifiedClick", "Noop", true)
	end
	if not self:IsHooked(GameTooltip, "OnTooltipSetItem") then
		self:SecureHookScript(GameTooltip, "OnTooltipSetItem")
	end

	createOrUpdateSetName = input
	StaticPopup_Show("ClosetGnomeAdd")
end

function ClosetGnome:CancelCreateSet()
	self:CharacterFrame_OnHide()
end

function ClosetGnome:AddSetFromDoll(name)
	local slots = {}
	local icon = nil
	for slot, active in pairs(activeSlots) do
		if slot == "icon" then
			icon = active
		elseif active then
			table.insert(slots, slot)
		end
	end
	if missingItems then
		for slot in pairs(missingItems) do
			table.insert(slots, slot)
		end
	end
	self:AddSet(name, slots, icon)
	slots = reclaim(slots)
	icon = nil

	self:CharacterFrame_OnHide()
end

function ClosetGnome:AddSet(name, slots, iconSlot)
	local concatedName = name:gsub(" ", "")
	if not name or name == "" or concatedName == "" then
		print(L["Please use a proper name for your set."])
		return
	end
	if self:HasSet(concatedName) then name = concatedName end

	local text = L["Added set: %s."]:format(name)
	local event = "AddSet"
	if self:HasSet(name) then
		text = L["Updating set: %s."]:format(name)
		event = "UpdateSet"
		for k in pairs(db.set[name]) do
			db.set[name][k] = nil
		end
	end

	for i, slot in pairs(slots) do
		local currentItem = missingItems and missingItems[slot] or getInvItem(slot)
		if not db.set[name] then db.set[name] = {} end
		db.set[name][slot] = currentItem or false
	end
	if not db.set[name] then return end

	if iconSlot then
		db.icons[name] = GetInventoryItemTexture("player", iconSlot)
	end

	triggerEvent(event, name)
	print(text)

	updateSetOptions()
end

function ClosetGnome:DeleteSet(name, verified)
	if not verified and not IsShiftKeyDown() then
		queuedForDelete = name
		StaticPopup_Show("ClosetGnomeDelete", name)
		return
	end

	queuedForDelete = nil
	if db.set[name] then
		db.set[name] = reclaim(db.set[name])
		db.icons[name] = nil
		clearBinding(name)

		triggerEvent("DeleteSet", name)
		print(L["Deleted set: %s."]:format(name))

		updateSetOptions()
	end
end

local function GetOppositeSlot(slot)
	if type(slot) ~= "number" then error("Only takes numbers.") end
	if slot == 11 then return 12
	elseif slot == 12 then return 11
	elseif slot == 13 then return 14
	elseif slot == 14 then return 13
	elseif slot == 16 then return 17
	elseif slot == 17 then return 16
	end
	return nil
end

local function processDeequipQueue()
	if not deequipQueue then deequipQueue = {} end
	for i, slot in pairs(deequipQueue) do
		PickupInventoryItem(slot)
		local toBag, toSlot = ClosetGnome:LocateFreeSlot()
		if toBag ~= nil then
			LockSlot(toBag, toSlot)
			PickupContainerItem(toBag, toSlot)
		else
			AutoEquipCursorItem()
			break
		end
	end
	deequipQueue = reclaim(deequipQueue)
	ClearSlotLocks()
end

local function ProcessEquipQueue()
	ClosetGnome:UnregisterEvent("ITEM_LOCK_CHANGED")
	if not equipQueue then return end
	for slot, item in pairs(equipQueue) do
		ClosetGnome:EquipItem(slot, item, nil)
	end
	equipQueue = reclaim(equipQueue)
end

local function QueueEquipItem(slot, item)
	if not equipQueue then equipQueue = {} end
	equipQueue[slot] = item
	ClosetGnome:RegisterEvent("ITEM_LOCK_CHANGED", ProcessEquipQueue)
end

-- ERR_2HANDED_EQUIPPED

local function ReallyWearSet(set, inCombat)
	local fullyEquipped = true
	for slot, item in pairs(set) do
		if not compareItems(getInvItem(slot), item) then
			if not inCombat or slot == 0 or slot == 16 or slot == 17 or slot == 18 then
				ClosetGnome:EquipItem(slot, item, nil)
			else
				fullyEquipped = false
			end
		end
	end
	return fullyEquipped
end

local function processQueue()
	if UnitIsDeadOrGhost("player") then return end
	if type(queuedSet) ~= "table" then
		ClosetGnome:UnregisterEvent("PLAYER_REGEN_ENABLED")
		ClosetGnome:UnregisterEvent("PLAYER_UNGHOST")
		ClosetGnome:UnregisterEvent("PLAYER_ALIVE")
		queuedSet = {}
		return
	end
	ClosetGnome:WearSet(unpack(queuedSet))
end

function ClosetGnome:WearSet(...)
	ClearCursor()
	if CursorHasItem() or CursorHasSpell() or (UnitIsDeadOrGhost("player") and not UnitIsFeignDeath("player")) then return false end

	local size = select("#", ...)
	if size < 1 then return false end

	local setToWear = nil
	local name = nil

	if size == 1 then
		name = select(1, ...)
		setToWear = db.set[name]
	else
		for i = 1, size do
			local set = select(i, ...)
			if type(set) ~= "string" or not db.set[set] then return false end
		end
		setToWear = {}
		for i = 1, size do
			name = select(i, ...)
			for k, v in pairs(db.set[name]) do
				setToWear[k] = v
			end
		end
	end
	if type(setToWear) ~= "table" then return false end

	if self:IsSetFullyEquipped(setToWear) then
		triggerEvent("WearSet", name)
		return true
	end

	if UnitAffectingCombat("player") then
		local fullyEquipped = false
		if db.alwaysWeapons then
			deequipQueue = reclaim(deequipQueue)
			fullyEquipped = ReallyWearSet(setToWear, true)
			processDeequipQueue()

			if self:IsSetFullyEquipped(setToWear) then
				if size > 1 then
					setToWear = reclaim(setToWear)
				end
				return true
			end
		end

		triggerEvent("PartlyWearSet", name)
		self:RegisterEvent("PLAYER_REGEN_ENABLED", processQueue)
		self:RegisterEvent("PLAYER_UNGHOST", processQueue)
		self:RegisterEvent("PLAYER_ALIVE", processQueue)
		if not fullyEquipped then
			print(L["In combat: %s queued for swap."]:format(name))
		end

		if size > 1 then
			setToWear = reclaim(setToWear)
		end

		for i = 1, size do
			table.insert(queuedSet, (select(i, ...)))
		end
		return true
	end

	self:UnregisterEvent("PLAYER_REGEN_ENABLED")
	self:UnregisterEvent("PLAYER_UNGHOST")
	self:UnregisterEvent("PLAYER_ALIVE")

	if self.db.profile.quips then
		-- I don't want the addon name in front for this one.
		-- Thanks to MrPlow for the color :)
		DEFAULT_CHAT_FRAME:AddMessage(self:GetQuip(name))
	end

	deequipQueue = reclaim(deequipQueue)
	ReallyWearSet(setToWear, nil)
	processDeequipQueue()
	triggerEvent("WearSet", name)

	if size > 1 then
		setToWear = reclaim(setToWear)
	end

	if type(queuedSet) == "table" then
		for i in ipairs(queuedSet) do
			table.remove(queuedSet, i)
		end
	end

	return true
end

-------------------------------------------------------------------------------
-- Utility                                                                   --
-------------------------------------------------------------------------------

function compareItems(item1, item2)
	if not item1 or not item2 then return (item1 == item2) end
	item1 = item1:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"):gsub(":(%-?%d+)|", ":%%-?%%d+|")
	return item2:match(item1)
end

function ClosetGnome:GetQuip(input)
	local rand = 1
	repeat rand = math.random(1, NUM_QUIPS) until L[rand] ~= nil
	return "|cffeda55f"..L[rand]:format("|cffd9d919"..input.."|cffeda55f").."|r"
end

function ClosetGnome:IsSetFullyEquipped(input)
	local set = nil
	if type(input) == "string" then
		set = db.set[input]
	elseif type(input) == "table" then
		set = input
	end
	if not set then return false end
	for slot, item in pairs(set) do
		-- Don't check the ammo slot at all.
		if slot > 0 and item and not IsEquippedItem(item) then
			return false
		end
	end
	return true
end

local function getItemEquipLoc(link)
	return select(9, GetItemInfo(link))
end

local function getItemSubtype(link)
	return select(7, GetItemInfo(link))
end

function ClosetGnome:IsNormalBag(bagId)
	if bagId == 0 or bagId == -1 then return true end
	local link = getInvItem(ContainerIDToInventoryID(bagId))
	if not link then return false end
	local linkId = select(3, link:find("item:(%d+)"))
	if not linkId then return false end
	local bagType = getItemSubtype(linkId)
	if bagType and bagType == INVTYPE_BAG then return true end
	return false
end

function ClosetGnome:LocateFreeSlot()
	for theBag = NUM_BAG_FRAMES, 0, -1 do
		if self:IsNormalBag(theBag) then
			local numSlot = GetContainerNumSlots(theBag)
			for theSlot = 1, numSlot, 1 do
				if not IsSlotLocked(theBag, theSlot) then
					local texture = GetContainerItemInfo(theBag, theSlot)
					if not texture then
						return theBag, theSlot
					end
				end
			end
		end
	end
	return nil
end

do
	local function rerun(tmp)
		ClosetGnome:EquipItem(tmp.slot, tmp.item, true)
		tmp.slot = nil
		tmp.item = nil
		tmp = nil
	end

	function ClosetGnome:EquipItem(slot, item, secondTry)
		if item == false then
			-- We need to find a free slot in the inventory for this item.
			-- If there's anything in the slot.
			local hasItem = getInvItem(slot)
			if hasItem ~= false then
				if not deequipQueue then deequipQueue = {} end
				table.insert(deequipQueue, slot)
			end
		else
			if slot == 17 and getInvItem(16) and getItemEquipLoc(getInvItem(16)) == "INVTYPE_2HWEAPON" then
				-- We can't equip off-hand items if we still have a 2h weapon equipped.  In such an event, we should deequip the 2h weapon first.
				local toBag, toSlot = self:LocateFreeSlot()
				if toBag ~= nil then
					PickupInventoryItem(16)
					PickupContainerItem(toBag, toSlot)
					LockSlot(toBag, toSlot)
				end
			end
			if slot == 16 and getInvItem(17) and getItemEquipLoc(item) == "INVTYPE_2HWEAPON" then
				-- Deequip the off-hand to the last bag slot when equipping a 2h weapon
				local toBag, toSlot = self:LocateFreeSlot()
				if toBag ~= nil then
					PickupInventoryItem(17)
					PickupContainerItem(toBag, toSlot)
					LockSlot(toBag, toSlot)
				end
			end
			local bagNum, slotNum = self:FindItem(item)
			local oppositeSlot = GetOppositeSlot(slot)
			local slotItem = getInvItem(slot)
			-- equip from bags
			if bagNum > -1 then
				ClearCursor()
				local locked = select(3, GetContainerItemInfo(bagNum, slotNum))
				if locked then
					QueueEquipItem(slot, item)
				else
					PickupContainerItem(bagNum, slotNum)
					EquipCursorItem(slot)
					LockSlot(bagNum, slotNum)
				end
			-- equip from other slot
			elseif oppositeSlot and compareItems(getInvItem(oppositeSlot), item) then
				if slot == 17 and slotItem and getItemEquipLoc(slotItem) ~= "INVTYPE_WEAPON" then
					local toBag, toSlot = self:LocateFreeSlot()
					if toBag ~= nil then
						ClearCursor()
						PickupInventoryItem(17)
						PickupContainerItem(toBag, toSlot)
						LockSlot(toBag, toSlot)
						QueueEquipItem(slot, item)
						return
					end
				end
				ClearCursor()
				PickupInventoryItem(oppositeSlot)
				PickupInventoryItem(slot)
			else
				if not secondTry and item then
					local tmp = {}
					tmp.slot = slot
					tmp.item = item
					self:ScheduleTimer(rerun, 0.1, tmp)
				else
					if activeSlots ~= nil and activeSlots[slot] then
						missingItems[slot] = item
						activeSlots[slot] = false
						slots[slot].texture:SetVertexColor(255, 255, 0)
					end
					print(L["Couldn't find %s in your inventory."]:format(tostring(item)))
				end
			end
		end
	end
end

function ClosetGnome:HasSet(name)
	return db.set[name] ~= nil
end

function ClosetGnome:FindItem(item)
	for i = NUM_BAG_FRAMES, 0, -1 do
		for j = GetContainerNumSlots(i), 1, -1 do
			if not IsSlotLocked(i, j) then
				local link = GetContainerItemLink(i, j)
				if link and compareItems(item, link) then return i, j end
			end
		end
	end
	return -1
end

do
	local tmp = {}
	function ClosetGnome:GetOutfitsUsingItem(itemLink)
		if type(itemLink) ~= "string" then return end
		for i in ipairs(tmp) do tmp[i] = nil end
		for name, items in pairs(db.set) do
			for k, item in pairs(items) do
				if compareItems(item, itemLink) then
					table.insert(tmp, name)
					break
				end
			end
		end
		return unpack(tmp)
	end
end

-------------------------------------------------------------------------------
-- Hooks                                                                     --
-------------------------------------------------------------------------------

-- Only hooked if we are showing the paper doll frame with set adding
function ClosetGnome:CharacterFrame_OnHide()
	activeSlots = reclaim(activeSlots)
	missingItems = reclaim(missingItems)
	for id, item in pairs(slots) do
		if item.frame then
			item.frame:EnableMouse(false)
			item.frame:Hide()
			item.texture:Hide()
		end
	end

	StaticPopup_Hide("ClosetGnomeAdd")

	self:UnhookAll()

	if Fizzle and Fizzle.db.profile.Border then
		Fizzle:UpdateItems()
	end

	if IsAddOnLoaded("oGlow") and type(oGlow) == "table" and getmetatable(oGlow) then
		oGlow.preventCharacter = nil
		if type(oGlow.updateCharacter) == "function" then
			oGlow.updateCharacter()
		end
	end

	CloseAllBags()
	HideUIPanel(CharacterFrame)
	CharacterFrame_OnHide()
end

function ClosetGnome:Noop() --[[ noop ]] end

-------------------------------------------------------------------------------
-- UI                                                                        --
-------------------------------------------------------------------------------

-- Hook this method and return true for any sets you do not want listed in the
-- tablet.
function ClosetGnome:HideSetFromUI(set)
	return false
end

-- Hook to provide for example coloring of different sets in the tablet.
function ClosetGnome:GetSetDisplayName(set)
	return set
end

